/*
 * Copyright © 2017 eqxiu.com 北京中网易企秀科技有限公司  All rights reserved.
 */
package cn.hermit.data.json;

import cn.hermit.core.BackableReader;
import cn.hermit.data.json.factory.HashJSONFactory;
import cn.hermit.data.json.factory.IdentityHashJSONFactory;
import cn.hermit.data.json.factory.IdentityOrderedJSONFactory;
import cn.hermit.data.json.factory.OrderedJSONFactory;
import cn.hermit.data.json.handler.JSONHandler;
import cn.hermit.data.json.model.JSONFactory;
import cn.hermit.data.json.validator.Validator;
import cn.hermit.util.StringUtils;
import cn.hermit.data.exception.ParserException;
import cn.hermit.data.json.handler.ContentHandler;
import cn.hermit.data.json.model.JSONDocument;

import java.io.Reader;
import java.util.logging.Logger;

/**
 * JSON Parser uses <i>Deterministic finite automaton</i> theory for parsing JSON string.
 * 
 * <p>
 * JSON Parser provides a lazy singleton pattern to getHttpRequest the instance.
 * It also provides new instance method to avoid some STATEful operations' conflicting.
 * </p>
 * 
 * <p>
 * It supports streaming API with {@link ContentHandler SAX syntax}.
 * And also, it can generate a document tree of JSON with {@link JSONHandler JSONHandler}.
 * Please see the method {@link JSONParser#parse(Reader, ContentHandler) parse(Reader, ContentHandler)}
 * </p>
 * 
 * <p>
 * In <a href="http://json.org" target="_blank">JSON specification</a>, 
 * there is no items about JSON order and JSON duplicated Keys. 
 * But some cases requires to keep order of JSON keys or keep duplicated keys.
 * The framework provides many factories to create various kinds of JSON Object, which allows duplicated keys or keeps keys order.
 * Please see the abstract class {@link JSONFactory JSONFactory}
 * and the package of {@link com.eqxiu.tiny.data.json.factory various factories }
 * </p>
 * 
 * <p>
 * Sometimes, JSON string need to be validated or the characters in 
 * JSON string need to be adjusted for some use case.
 * {@link Validator Validator} provides methods to validate keys and values.
 * The <tt>Validator</tt> also provide a chain to for a serious of validation.
 * </p>
 * 
 * <p>
 * The framework provides {@link com.eqxiu.tiny.data.log.Logger a simple logger}
 * which can adapt to third party logging system by {@link com.eqxiu.tiny.data.log.Logger#setDelegator(com.eqxiu.tiny.data.log.LogDelegator) setDelegator(LogDelegator)}.
 * 
 * </p>
 * 
 * @author Jack Gao (Chinese name : GAO JIANGUO, Email : linux.gjg@gmail.com)
 * @date 13 Jun, 2014
 */
public class JSONParser implements JSON {

    private static final Logger logger = Logger.getLogger(JSONParser.class.getName());

    private static JSONParser INSTANCE;

    private JSONParser() {

    }

    /**
     * Lazy singleton pattern.
     * 
     * <p>
     * The singleton instance is treated as a tool to handle none context or stateful less
     * parsing.
     * </p>
     * 
     * @return
     * 		The singleton instance of JSON Parser.
     */
    public static JSONParser getInstance() {
        if (INSTANCE == null) {
            synchronized (JSONParser.class) {
                if (INSTANCE == null) {
                    INSTANCE = new JSONParser();
                }
            }
        }
        return INSTANCE;
    }

    /**
     * Create a new instance of JSON Parser.
     * <p>Provide a consistent API to create a instance</p>
     * 
     * @return
     * 		New instance of JSON Parser.
     */
    public static JSONParser newInstance() {
        return new JSONParser();
    }

    /**
     * Parse JSON string with a reader.
     * 
     * @param reader
     * 			Reader of JSON string.
     * @return
     * @throws ParserException
     */
    public JSONDocument parse(Reader reader) throws ParserException {
        JSONHandler handler = new JSONHandler();
        parse(reader, handler);
        return handler.getDocument();
    }

    /**
     * Parse JSON string with a reader and JSONFactory.
     * 
     * @see JSONFactory JSONFactory
     * @see HashJSONFactory HashJSONFactory
     * @see IdentityHashJSONFactory IdentityHashJSONFactory
     * @see IdentityOrderedJSONFactory IdentityOrderedJSONFactory
     * @see OrderedJSONFactory OrderedJSONFactory
     * 
     * @param reader
     * 		Reader of JSON string
     * @param jsonFactory
     * 		JSONFactory to create JSON Object
     * @return
     * 		Document of JSON 
     * @throws ParserException
     */
    public JSONDocument parse(Reader reader, JSONFactory jsonFactory) throws ParserException {
        JSONHandler handler = new JSONHandler(jsonFactory);
        parse(reader, handler);
        return handler.getDocument();
    }

    /**
     * Parse JSON string with a reader and ContentHandler
     * 
     * <p>
     * It provides a streaming API with ContentHandler in SAX syntax.
     * </p>
     * 
     * @param reader
     * 		Reader of JSON string
     * @param handler
     * 		ContentHandler in SAX syntax
     * @throws ParserException
     */
    public void parse(Reader reader, ContentHandler handler) throws ParserException {
        BackableReader br = new BackableReader(reader);
        parse(JSON.STATUS_JSON, br, handler);
    }

    //Deterministic finite automaton.
    //Recursively parse.
    private void parse(byte status, BackableReader br, ContentHandler handler) throws ParserException {
        boolean inArray = JSON.STATUS_ARRAY == status;
        if (JSON.STATUS_JSON == status) {
            handler.startDocument();
        }
        try {
            while (true) {
                String token = br.read(TOKEN_DELIMS);
                if (StringUtils.isEmpty(token)) {
                    if (status == JSON.STATUS_JSON) {
                        handler.endDocument();
                    } else {
                        invalidJSON(br);
                    }
                    return;
                }
                char delim = token.charAt(token.length() - 1);
                if (token.length() > 1) {
                    token = token.substring(0, token.length() - 1);
                } else {
                    token = null;
                }
                switch (delim) {
                case '{':
                    if (status == JSON.STATUS_OBJECT) {
                        invalidJSON(br);
                    }
                    if (!StringUtils.isEmpty(token)) {
                        invalidJSON(br);
                    }
                    handler.startObject(inArray);
                    parse(JSON.STATUS_OBJECT, br, handler);
                    if (status == JSON.STATUS_ENTRY) {
                        status = JSON.STATUS_OBJECT;
                    }
                    break;
                case '}':
                    if (status == JSON.STATUS_ARRAY || status == JSON.STATUS_JSON) {
                        invalidJSON(br);
                    }
                    if (!StringUtils.isEmpty(token)) {
                        if (status != JSON.STATUS_ENTRY) {
                            invalidJSON(br);
                        }
                        handler.value(token.trim(), false, inArray);
                    }
                    handler.endObject(inArray);
                    return;
                case '[':
                    if (status == JSON.STATUS_OBJECT) {
                        invalidJSON(br);
                    }
                    if (!StringUtils.isEmpty(token)) {
                        invalidJSON(br);
                    }
                    handler.startArray(inArray);
                    parse(JSON.STATUS_ARRAY, br, handler);
                    if (status == JSON.STATUS_ENTRY) {
                        status = JSON.STATUS_OBJECT;
                    }
                    break;
                case ']':
                    if (status != JSON.STATUS_ARRAY) {
                        invalidJSON(br);
                    }
                    if (!StringUtils.isEmpty(token)) {
                        handler.value(token.trim(), false, inArray);
                    }
                    handler.endArray(inArray);
                    return;
                case ':':
                    if (status != JSON.STATUS_OBJECT) {
                        invalidJSON(br);
                    }
                    status = JSON.STATUS_ENTRY;
                    break;
                case '"':
                    if (status == JSON.STATUS_JSON) {
                        invalidJSON(br);
                    }
                    if (!StringUtils.isEmpty(token)) {
                        invalidJSON(br);
                    }
                    token = br.readInQuotes('"');
                    if (token == null) {
                        invalidJSON(br);
                    }
                    switch (status) {
                    case JSON.STATUS_OBJECT:
                        handler.key(token);
                        break;
                    case JSON.STATUS_ARRAY:
                        handler.value(token, true, inArray);
                        break;
                    case JSON.STATUS_ENTRY:
                        handler.value(token, true, inArray);
                        status = JSON.STATUS_OBJECT;
                        break;
                    default:
                        break;
                    }
                    break;
                case ',':
                    if (status == JSON.STATUS_JSON) {
                        invalidJSON(br);
                    }
                    if (StringUtils.isEmpty(token)) {
                        break;
                    }
                    token = token.trim();
                    // if (!token.matches("[\\d\\w]+")) {
                    // invalidJSON(br);
                    // }
                    handler.value(token.trim(), false, inArray);
                    if (status == JSON.STATUS_ENTRY) {
                        status = JSON.STATUS_OBJECT;
                    }
                    break;
                default:
                    invalidJSON(br);
                    break;
                }
            }
        } catch (ParserException e) {
            throw e;
        } catch (Exception e) {
            error(e, br);
        }

    }

    private void invalidJSON(BackableReader br) throws ParserException {
        error("Invalid JSON at[{0}]", br.getCacheString());
    }

    private void error(Exception e, BackableReader br) throws ParserException {
        String errorMsg = StringUtils.errorMessage(e);
        error("Error happened at[{0}]\n" + errorMsg, br.getCacheString());
    }

    private void error(String errorMsg, String... params) throws ParserException {
        throw new ParserException(StringUtils.mergeParams(errorMsg, params));
    }
}